﻿//-----------------------------------------------------------------------
// <copyright file="ClipboardModule.Paste.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace App.Modules.Clipboard
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using App.Modules.EntityFactory;
    using App.Modules.Message;
    using App.Modules.Scene;
    using TileTool.Action;
    using TileTool.Action.Message;
    using TileTool.Clipboard;
    using TileTool.DataBag;
    using TileTool.DataBag.Generic;
    using TileTool.Log;
    using TileTool.Scene;
    using TileTool.Scene.Action;
    using TileTool.Scene.Entity;

    /// <summary>
    /// Application clipboard module paste specific code. 
    /// </summary>
    public partial class ClipboardModule
    {
        #region Methods - Private

        /// <summary>
        /// Attempt to paste the data from the clipboard.
        /// </summary>
        private void PasteClipboardData()
        {
            IList<EntityCreateData> createDataList = this.BuildCreateData();
            if (createDataList == null)
            {
                return;
            }

            if (!this.ValidateCreateData(createDataList))
            {
                return;
            }

            this.RemapGuids(createDataList);

            IEntityCollection addCollection = this.AllocateObjectsFromCreateData(createDataList);
            if (addCollection == null)
            {
                return;
            }

            IDictionary<Guid, IEntity> guidToEntityDict = this.BuildGuidToEntityDictionary(addCollection);
            if (guidToEntityDict == null)
            {
                return;
            }

            SceneModule sceneModule = this.GetModule<SceneModule>();
            ISceneManager scene = sceneModule.Scene;
            AddRemoveEntityAction addAction = new AddRemoveEntityAction(scene, addCollection, true);
            SetSceneSelectionAction selectionAction = this.BuildSelectionAction(createDataList, guidToEntityDict);
            SetParentEntityAction setParentAction = this.BuildSetParentAction(createDataList, guidToEntityDict);
            CompoundAction compoundAction = new CompoundAction();

            compoundAction.AddAction(addAction);
            compoundAction.AddAction(selectionAction);
            compoundAction.AddAction(setParentAction);
            compoundAction.Do();

            ActionMessage msg = new ActionMessage(compoundAction);
            MessageModule messageModule = this.GetModule<MessageModule>();

            messageModule.MessageRouter.SendMessage(msg);
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Build entity create data from the clipboard.
        /// </summary>
        /// <returns>Returns a list of entity create data on success or null if there was an error.</returns>
        private IList<EntityCreateData> BuildCreateData()
        {
            ClipboardData data = this.clipboard.GetData();
            if (data == null)
            {
                LogSystem.LogError(LogGroup, "BuildCreateData() - Failed to get clipboard data.");
                return null;
            }

            bool success = true;
            ClipboardDataInfo info = this.BuildClipboardDataInfo(data);
            IDictionary<string, IDataBag> dataDict = data.DataDictionary;
            IList<EntityCreateData> createDataList = new List<EntityCreateData>();

            for (int i = 0; i < info.ObjectCount; ++i)
            {
                IDataBag objectBag;
                string objectName = string.Format(ObjectKeyFormat, i);

                if (!dataDict.TryGetValue(objectName, out objectBag))
                {
                    success = false;
                    LogSystem.LogError(LogGroup, "BuildCreateData() - Failed to find object: " + objectName);
                    break;
                }

                IDataBagValue<string> componentNameValue;
                if (!objectBag.TryGetValue(EntityDataBagKeys.ComponentName, out componentNameValue))
                {
                    success = false;
                    LogSystem.LogError(LogGroup, "BuildCreateData() - Failed to find object component name for object: " + objectName);
                    break;
                }

                EntityCreateData createData = new EntityCreateData(objectBag);
                if (createData == null)
                {
                    success = false;
                    LogSystem.LogError(LogGroup, "BuildCreateData() - Failed to build create data for object: " + objectName);
                    break;
                }

                createDataList.Add(createData);
            }

            if (!success)
            {
                return null;
            }

            return createDataList;
        }

        /// <summary>
        /// Validate the list of create data that just came from the clipboard.
        /// </summary>
        /// <param name="createDataList">List of create data allocated from the clipboard.</param>
        /// <returns>Returns true if the data is valid.</returns>
        private bool ValidateCreateData(IList<EntityCreateData> createDataList)
        {
            HashSet<Guid> guidSet = new HashSet<Guid>();

            // First pass make sure we have unique GUIDs.
            foreach (EntityCreateData createData in createDataList)
            {
                Guid guid = createData.Guid;

                if (guid == Guid.Empty)
                {
                    LogSystem.LogError(LogGroup, "ValidateCreateData() - Empty GUID should never be empty.");
                    return false;
                }

                if (guidSet.Contains(guid))
                {
                    LogSystem.LogError(LogGroup, "ValidateCreateData() -Duplicate GUID found: " + guid);
                    return false;
                }

                guidSet.Add(guid);
            }

            // Second pass make sure that if we have a parent GUID that actually exists.
            foreach (EntityCreateData createData in createDataList)
            {
                Guid guid = createData.ParentGuid;
                if (guid == Guid.Empty)
                {
                    continue;
                }

                if (!createData.ParentGuidValid)
                {
                    LogSystem.LogError(LogGroup, "ValidateCreateData() - Create data has an invalid parent GUID.");
                    return false;
                }

                if (!guidSet.Contains(guid))
                {
                    LogSystem.LogError(LogGroup, "ValidateCreateData() - Failed to find parent GUID: " + guid);
                    return false;
                }

                guidSet.Add(guid);
            }

            return true;
        }

        /// <summary>
        /// Remap all GUIDs that were given to us from the clipboard to new GUIDs.
        /// 
        /// This is done since the GUIDs in the clipboard could have been crafted to
        /// collide with existing GUIDs.
        /// </summary>
        /// <param name="createDataList">Create data list from the clipboard.</param>
        private void RemapGuids(IList<EntityCreateData> createDataList)
        {
            IDictionary<Guid, Guid> oldToNewMap = new Dictionary<Guid, Guid>();

            foreach (EntityCreateData createData in createDataList)
            {
                Guid guid = createData.Guid;

                oldToNewMap[guid] = Guid.NewGuid();
            }

            foreach (EntityCreateData createData in createDataList)
            {
                Debug.Assert(oldToNewMap.ContainsKey(createData.Guid), "Entity GUID must exist.");

                createData.Guid = oldToNewMap[createData.Guid];

                if (createData.ParentGuid != Guid.Empty)
                {
                    Debug.Assert(oldToNewMap.ContainsKey(createData.ParentGuid), "Entity parent GUID must exist.");

                    createData.ParentGuid = oldToNewMap[createData.ParentGuid];
                }
            }
        }

        /// <summary>
        /// Allocate objects from the passed create data list.
        /// </summary>
        /// <param name="createDataList">Create data list to allocate from.</param>
        /// <returns>Returns a collection of allocated objects or null if there was an error.</returns>
        private IEntityCollection AllocateObjectsFromCreateData(IList<EntityCreateData> createDataList)
        {
            bool success = true;
            EntityFactoryModule factory = this.GetModule<EntityFactoryModule>();
            EntityFactoryModule entityFactoryModule = this.GetModule<EntityFactoryModule>();
            IEntityCollection addCollection = new EntityCollection();

            foreach (EntityCreateData createData in createDataList)
            {
                IDataBag dataBag = createData.Bag;
                IDataBagValue<string> componentNameValue;
                if (!dataBag.TryGetValue(EntityDataBagKeys.ComponentName, out componentNameValue))
                {
                    throw new InvalidOperationException("Component name existence should be validated earlier on in the paste process!");
                }

                string componentName = componentNameValue.Value;
                IEntity res = entityFactoryModule.CreateEntity(componentName, createData);
                if (res == null)
                {
                    LogSystem.LogError(LogGroup, "AllocateObjectsFromCreateData() - Failed to allocate object with component name: " + componentName);
                    success = false;
                    break;
                }

                addCollection.Add(res);
            }

            if (!success)
            {
                return null;
            }

            return addCollection;
        }

        /// <summary>
        /// Build a dictionary that maps from a GUID to its associated entity.
        /// </summary>
        /// <param name="addCollection">Collection of entities that we are going to add to the scene.</param>
        /// <returns>GUID to entity dictionary or null if there was an error.</returns>
        private IDictionary<Guid, IEntity> BuildGuidToEntityDictionary(IEntityCollection addCollection)
        {
            IDictionary<Guid, IEntity> dict = new Dictionary<Guid, IEntity>();

            foreach (IEntity entity in addCollection)
            {
                Debug.Assert(!dict.ContainsKey(entity.Guid), "Should have been checked earlier.");
                Debug.Assert(entity.Guid != Guid.Empty, "Should have been checked earlier.");

                dict.Add(entity.Guid, entity);
            }

            return dict;
        }

        /// <summary>
        /// Will build a selection action which will select the entities highest up the
        /// parent child tree from the set we are pasting. This is to property handle
        /// groups and parents.
        /// </summary>
        /// <param name="createDataList">Create data list used to allocate objects.</param>
        /// <param name="guidToEntity">GUID to entity dictionary.</param>
        /// <returns>Returns the selection action. Should not return null.</returns>
        private SetSceneSelectionAction BuildSelectionAction(IList<EntityCreateData> createDataList, IDictionary<Guid, IEntity> guidToEntity)
        {
            IEntityCollection selection = new EntityCollection();

            foreach (EntityCreateData createData in createDataList)
            {
                Guid entityGuid = createData.Guid;
                Guid parentGuid = createData.ParentGuid;

                // If we have a parent, the parent will be selected, not us.
                if (parentGuid != Guid.Empty)
                {
                    continue;
                }

                // If we don't have a parent, we will be selected.
                selection.Add(guidToEntity[entityGuid]);
            }

            SceneModule sceneModule = this.GetModule<SceneModule>();
            ISceneManager scene = sceneModule.Scene;
            SetSceneSelectionAction selectionAction = new SetSceneSelectionAction(scene, selection);

            return selectionAction;
        }

        /// <summary>
        /// Will build a set parent action which will handle setting the parent correctly for all
        /// entities.
        /// groups and parents.
        /// </summary>
        /// <param name="createDataList">Create data list used to allocate objects.</param>
        /// <param name="guidToEntity">GUID to entity dictionary.</param>
        /// <returns>Returns the set parent action. Should not return null.</returns>
        private SetParentEntityAction BuildSetParentAction(IList<EntityCreateData> createDataList, IDictionary<Guid, IEntity> guidToEntity)
        {
            SceneModule sceneModule = this.GetModule<SceneModule>();
            ISceneManager scene = sceneModule.Scene;
            SetParentEntityAction res = new SetParentEntityAction(scene);

            foreach (EntityCreateData createData in createDataList)
            {
                Guid parentGuid = createData.ParentGuid;
                if (parentGuid == Guid.Empty)
                {
                    continue;
                }

                Guid entityGuid = createData.Guid;
                IEntity entity = guidToEntity[entityGuid];
                IEntity parent = guidToEntity[parentGuid];

                res.AddEntity(entity, parent);
            }

            return res;
        }

        /// <summary>
        /// Given clipboard data, build the clipboard data info class
        /// from it and validate it.
        /// </summary>
        /// <param name="data">Clipboard data to build from.</param>
        /// <returns>Returns the clipboard data info class or null if we were unable to build it.</returns>
        private ClipboardDataInfo BuildClipboardDataInfo(ClipboardData data)
        {
            IDataBag infoBag;
            IDictionary<string, IDataBag> dict = data.DataDictionary;
            if (!dict.TryGetValue(InfoBagKey, out infoBag))
            {
                LogSystem.LogError(LogGroup, "BuildClipboardDataInfo() - Failed to find info bag with key: " + InfoBagKey);
                return null;
            }

            ClipboardDataInfo info = DataBagObjectSerialization.DataBagToObject<ClipboardDataInfo>(infoBag);
            if (info == null)
            {
                LogSystem.LogError(LogGroup, "BuildClipboardDataInfo() - Failed to build clipboard data info.");
                return null;
            }

            if (!Enum.IsDefined(typeof(ClipboardDataType), info.DataType))
            {
                LogSystem.LogError(LogGroup, "BuildClipboardDataInfo() - Invalid clipboard data type: " + info.DataType);
                return null;
            }

            if (info.DataType != (int)ClipboardDataType.Entity)
            {
                LogSystem.LogError(LogGroup, "BuildClipboardDataInfo() - Unsupported data type: " + info.DataType);
                return null;
            }

            return info;
        }

        #endregion
    }
}
